Popular Searches
Popular Course Categories
Popular Courses

Choosing the Right State Management Approach for Mobile Apps

Choosing the Right State Management Approach for Mobile Apps

State Management in React Native

Choosing the Right State Management Approach for Mobile Apps

Course: React Native Training

Topic: Choosing the Right State Management Approach for Mobile Apps

Course Link: React Native Training – JustAcademy

Demo Link: Register for Course Demo

Website: JustAcademy Official Website


1. Introduction to State Management in Mobile Apps

State management is the process of storing, updating, and sharing information that determines how an application behaves and what appears on the screen. In React Native, state may represent a user's login status, shopping cart, selected theme, form input, product list, navigation preferences, or data retrieved from a remote API.

As a mobile application grows, state can become difficult to manage across multiple components and screens. A small app may work well with React's useState hook, while a larger app may benefit from useReducer, Context API, Redux Toolkit, Zustand, or a server-state library such as TanStack Query.

There is no single state management solution that is best for every application. The correct choice depends on the type of state, the number of components that use it, the complexity of updates, caching requirements, debugging needs, and the team's experience.

1.1 Learning Objectives

  • Understand the meaning and importance of state management.
  • Identify local, shared, global, server, and persistent state.
  • Understand React Native's built-in state management options.
  • Compare useState, useReducer, Context API, Redux Toolkit, Zustand, and TanStack Query.
  • Choose a state management solution according to project requirements.
  • Understand state updates, immutability, and component re-rendering.
  • Manage authentication, themes, forms, shopping carts, and API data.
  • Understand persistence, performance, testing, and debugging.
  • Apply practical state management patterns in a React Native application.

2. What Is State?

State is information that can change over time and affect an application's behavior or rendered interface. When state changes through React's supported mechanisms, React can render the affected components with updated information.

2.1 Examples of State in Mobile Applications

Application Feature Example State Possible Approach
Login screen Email, password visibility, validation errors useState or a form library
Theme settings Light or dark theme Context API or a global store
Shopping cart Products, quantities, subtotal useReducer, Context, Zustand, or Redux Toolkit
Product catalogue Products retrieved from a server TanStack Query or another server-state solution
Authentication Current user and session status Context or a global store, with secure token handling
Navigation UI Selected tab or open drawer Local state or navigation state
App preferences Language, display settings, selected options Local state plus persistence when needed

2.2 Why State Management Matters

  • Consistent UI: Components display information that reflects the current state.
  • Data sharing: Multiple screens can access shared information.
  • Maintainability: Related updates can be organized in predictable places.
  • Performance: Appropriate state placement can avoid unnecessary re-rendering.
  • Testing: Predictable updates are easier to verify.
  • Debugging: Clear state transitions help developers understand unexpected behavior.
  • Persistence: Selected state can be restored when the application restarts.

3. Understanding Different Types of State

Before selecting a library, identify what type of information the application needs to manage. Different categories have different requirements and should not automatically be placed in one global store.

3.1 Local Component State

Local state belongs to one component or a small group of closely related components. It is suitable for temporary interface information.

Examples include:

  • Whether a modal is open.
  • The value of a text input.
  • Whether a password is visible.
  • The currently selected item in a small component.
import React, { useState } from "react";
import { Button, Text, View } from "react-native";

export default function Counter() {
  const [count, setCount] = useState(0);

  return (
    
      Count: {count}
      

This example uses useState because the counter value is needed only by the component itself.

3.2 Shared Component State

Shared state is information required by multiple related components. The state can often be moved to their closest common parent and passed down through props.

For example, a product filter component and a product list may both need the selected category. The parent can own the category state and pass the value and update function to each child.

3.3 Global Application State

Global state is information used across unrelated parts of the application. Examples include selected application theme, shared authentication status, or a shopping cart used by several screens.

Global state can be managed with Context API, Redux Toolkit, Zustand, or another appropriate store. However, globalizing every state variable can make an application harder to understand and test.

3.4 Server State

Server state is information owned by a remote service. Examples include product lists, user profiles, notifications, and order histories.

Server data can become stale because another user or service may update it. Server-state tools such as TanStack Query can help with fetching, caching, refetching, invalidation, and request status management.

3.5 Persistent State

Persistent state is information that should remain available after an application closes or restarts. Examples include language preference, selected theme, or non-sensitive settings.

Persistence is separate from the in-memory state management choice. A React state hook or global store does not automatically save values to device storage.

3.6 Navigation State

Navigation state represents the current route, navigation history, and navigation parameters. Navigation libraries such as React Navigation manage this information. Avoid duplicating navigation state in a separate global store unless there is a specific, justified requirement.

4. The State Management Decision Process

The best starting point is the simplest solution that meets the application's current needs. Add complexity only when the requirements justify it.

4.1 Step-by-Step Selection Process

  1. Identify the data: Determine whether it is temporary UI state, shared client state, server data, navigation state, or persistent preferences.
  2. Identify the consumers: Check how many components and screens need the information.
  3. Review update complexity: Decide whether changes are simple assignments or complex transitions involving several related values.
  4. Evaluate remote data requirements: Determine whether caching, background refresh, retries, or invalidation are necessary.
  5. Consider persistence: Decide which values must survive an app restart and where they should be stored.
  6. Consider debugging and testing: Evaluate how easily developers can trace state changes and test behavior.
  7. Measure performance: Identify real re-rendering or data-loading bottlenecks rather than optimizing prematurely.
  8. Choose and document the approach: Explain which state belongs where and why.

4.2 Quick Decision Guide

Requirement Recommended Starting Point
A value used by one component useState
Several related state transitions useReducer
A small number of shared values across a component tree Context API
Complex shared client state and structured update logic Redux Toolkit
A lightweight shared store with a simple API Zustand
Remote data that needs caching and synchronization TanStack Query or a similar server-state library
Simple preferences that survive app restarts State plus suitable device storage
Form validation and complex form workflows Local state or a dedicated form library

5. Managing State with useState

The useState hook is built into React and is appropriate for many simple state management tasks. It returns the current state value and a setter function that schedules an update.

5.1 Basic Syntax

const [value, setValue] = useState(initialValue);
  • value: The current state value.
  • setValue: The function used to request a state update.
  • initialValue: The initial state value.

5.2 Managing Form Input

import React, { useState } from "react";
import { Text, TextInput, View } from "react-native";

export default function NameForm() {
  const [name, setName] = useState("");

  return (
    
      
      Hello, {name || "Guest"}!
    
  );
}

This is an example of local state. The value belongs to one form component and does not require a global state library.

5.3 Updating State Correctly

When the next state depends on the previous value, use the functional updater form. It ensures the update is calculated from the latest queued state value.

setCount((previousCount) => previousCount + 1);

For objects and arrays, create a new object or array rather than mutating the existing state directly.

setUser((previousUser) => ({
  ...previousUser,
  name: "Aarav"
}));

setItems((previousItems) => [
  ...previousItems,
  newItem
]);

5.4 Advantages of useState

  • Built into React with no additional library.
  • Easy to learn and implement.
  • Works well for forms and small UI interactions.
  • Supports objects, arrays, strings, numbers, booleans, and other values.

5.5 Limitations of useState

  • Many related state variables can become difficult to coordinate.
  • Sharing state across distant screens can require additional architecture.
  • It does not provide a global cache for server data.
  • It does not automatically persist state across application restarts.

6. Managing Complex State with useReducer

The useReducer hook is useful when state updates involve several related values or when an application has multiple predictable transitions. It centralizes update logic in a reducer function.

6.1 Reducer Concepts

  • State: The current collection of values.
  • Action: An object describing what happened.
  • Reducer: A function that calculates the next state from the current state and action.
  • Dispatch: A function used to send an action to the reducer.

6.2 Example: Managing a Shopping Cart

import React, { useReducer } from "react";
import { Button, Text, View } from "react-native";

const initialCart = {
  items: []
};

function cartReducer(state, action) {
  switch (action.type) {
    case "ADD_ITEM": {
      const existing = state.items.find(
        (item) => item.id === action.product.id
      );

      if (existing) {
        return {
          ...state,
          items: state.items.map((item) =>
            item.id === action.product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          )
        };
      }

      return {
        ...state,
        items: [
          ...state.items,
          { ...action.product, quantity: 1 }
        ]
      };
    }

    case "REMOVE_ITEM":
      return {
        ...state,
        items: state.items.filter(
          (item) => item.id !== action.id
        )
      };

    case "CLEAR_CART":
      return initialCart;

    default:
      return state;
  }
}

export default function CartExample() {
  const [cart, dispatch] = useReducer(
    cartReducer,
    initialCart
  );

  const product = {
    id: 1,
    name: "Wireless Headphones",
    price: 1500
  };

  return (
    
      {product.name}
      <button title="Add to Cart">
          dispatch({
            type: "ADD_ITEM",
            product
          })
        }
      />
      Cart items: {cart.items.length}
      </button>

This example uses useReducer to manage related cart transitions. A production cart would also calculate quantities, validate prices using trusted server data, handle stock restrictions, and synchronize checkout operations with the backend.

6.3 When Should You Use useReducer?

  • Several state values change together.
  • Updates follow defined actions or transitions.
  • State logic is becoming difficult to maintain across multiple setters.
  • You want reducer logic that can be tested independently.

7. Sharing State with the Context API

Context API allows a component to provide a value to descendants without passing it through every intermediate component as props. It is useful for shared values such as theme, locale, or a current-user context.

7.1 Creating a Context

import { createContext } from "react";

export const ThemeContext = createContext(null);

7.2 Providing and Reading Context

import React, { createContext, useContext, useState } from "react";
import { Button, Text, View } from "react-native";

const ThemeContext = createContext(null);

function ThemeProvider({ children }) {
  const [darkMode, setDarkMode] = useState(false);

  const value = {
    darkMode,
    toggleTheme: () =>
      setDarkMode((current) => !current)
  };

  return (
    
      {children}
    
  );
}

function ThemeScreen() {
  const theme = useContext(ThemeContext);

  return (
    
      
        Current theme: {theme.darkMode ? "Dark" : "Light"}
      
      

The provider exposes a shared theme value and a function to change it. In a complete application, the provider would normally wrap the relevant part of the component tree and theme styles would be organized consistently.

7.3 Advantages of Context API

  • Built into React.
  • Reduces prop drilling for values needed by distant descendants.
  • Useful for themes, localization, and shared configuration.
  • Can be combined with useReducer for more structured state transitions.

7.4 Limitations of Context API

  • It is not a complete replacement for every state management requirement.
  • Consumers may re-render when the provided value changes.
  • Frequently changing values may require careful provider design.
  • It does not automatically provide server-state caching or request deduplication.
  • Too many providers can make the application structure harder to follow if poorly organized.

8. Managing Global State with Redux Toolkit

Redux Toolkit is the recommended standard approach for writing Redux logic. It provides utilities for creating slices, reducers, actions, and stores while reducing common boilerplate.

8.1 When Redux Toolkit Is Appropriate

  • The application has complex shared client state.
  • Multiple screens depend on coordinated state updates.
  • Developers need a predictable state transition model.
  • State changes benefit from Redux DevTools and established debugging workflows.
  • The project has multiple developers who need consistent state conventions.

8.2 Installing Redux Toolkit

npm install @reduxjs/toolkit react-redux

Use the installation instructions appropriate to your React Native project and build setup.

8.3 Creating a Redux Slice

// features/counter/counterSlice.js

import { createSlice } from "@reduxjs/toolkit";

const counterSlice = createSlice({
  name: "counter",
  initialState: {
    value: 0
  },
  reducers: {
    increment(state) {
      state.value += 1;
    },
    decrement(state) {
      state.value -= 1;
    },
    reset(state) {
      state.value = 0;
    }
  }
});

export const {
  increment,
  decrement,
  reset
} = counterSlice.actions;

export default counterSlice.reducer;

Redux Toolkit uses Immer internally in createSlice, allowing reducer logic to use mutation-like syntax while producing immutable state updates.

8.4 Configuring the Redux Store

// app/store.js

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer
  }
});

8.5 Providing the Store

// App.js

import React from "react";
import { Provider } from "react-redux";
import { store } from "./app/store";
import CounterScreen from "./CounterScreen";

export default function App() {
  return (
    
      
    
  );
}

8.6 Reading and Updating Redux State

import React from "react";
import { Button, Text, View } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { increment, decrement } from "./features/counter/counterSlice";

export default function CounterScreen() {
  const count = useSelector(
    (state) => state.counter.value
  );
  const dispatch = useDispatch();

  return (
    
      Count: {count}
      <button title="Increase"> dispatch(increment())}
      />
      </button>

8.7 Advantages of Redux Toolkit

  • Centralized shared client state.
  • Predictable state transitions and organized reducer logic.
  • Useful debugging and developer tooling.
  • Good support for larger teams and complex applications.
  • Includes createAsyncThunk and can integrate with RTK Query for asynchronous workflows and server data.

8.8 Limitations of Redux Toolkit

  • Requires store configuration and additional concepts.
  • Can be unnecessary for small applications with little shared state.
  • Developers must understand slices, reducers, selectors, and dispatching.
  • Server-state caching needs an appropriate approach, such as RTK Query, rather than treating every remote request as ordinary client state.

9. Managing Shared State with Zustand

Zustand is a lightweight state management library that provides a store and hooks for reading or updating shared state. It can be useful when an application needs shared state without adopting Redux's full structure.

9.1 Installing Zustand

npm install zustand

9.2 Creating a Store

// stores/useCartStore.js

import { create } from "zustand";

export const useCartStore = create((set) => ({
  items: [],

  addItem: (product) =>
    set((state) => {
      const existing = state.items.find(
        (item) => item.id === product.id
      );

      if (existing) {
        return {
          items: state.items.map((item) =>
            item.id === product.id
              ? { ...item, quantity: item.quantity + 1 }
              : item
          )
        };
      }

      return {
        items: [
          ...state.items,
          { ...product, quantity: 1 }
        ]
      };
    }),

  clearCart: () => set({ items: [] })
}));

9.3 Using the Store in a Screen

import React from "react";
import { Button, Text, View } from "react-native";
import { useCartStore } from "./stores/useCartStore";

export default function CartScreen() {
  const items = useCartStore((state) => state.items);
  const addItem = useCartStore((state) => state.addItem);
  const clearCart = useCartStore((state) => state.clearCart);

  const product = {
    id: 2,
    name: "Mobile Stand",
    price: 400
  };

  return (
    
      Items in cart: {items.length}
      <button title="Add Product"> addItem(product)}
      />
      </button>

The selectors access only the parts of the store that the component needs. Selector design and equality behavior should be considered when optimizing re-renders.

9.4 Advantages of Zustand

  • Small API and relatively little boilerplate.
  • Suitable for shared state used across screens.
  • Supports selecting specific pieces of state.
  • Can be combined with middleware for persistence and development workflows.

9.5 Limitations of Zustand

  • Requires an additional dependency.
  • Projects still need clear conventions for store organization and update logic.
  • Persistence and server-state synchronization require deliberate configuration.
  • Large teams may prefer more explicit architectural conventions provided by other tools.

10. Managing Server State with TanStack Query

Remote API data has different requirements from ordinary local state. It may need caching, refetching, background updates, retries, and invalidation after a mutation. TanStack Query is designed to manage these server-state concerns.

10.1 When to Use TanStack Query

  • Several screens use the same API data.
  • Data needs to be refreshed when it becomes stale.
  • Requests should be cached or deduplicated.
  • Mutations need to update or invalidate cached queries.
  • Loading and error states should be managed consistently.

10.2 Installing the Library

npm install @tanstack/react-query

10.3 Configuring QueryClientProvider

import React from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import ProductsScreen from "./ProductsScreen";

const queryClient = new QueryClient();

export default function App() {
  return (
    
      
    
  );
}

10.4 Fetching Products with useQuery

import React from "react";
import { ActivityIndicator, Text, View } from "react-native";
import { useQuery } from "@tanstack/react-query";

async function fetchProducts() {
  const response = await fetch(
    "https://dummyjson.com/products"
  );

  if (!response.ok) {
    throw new Error("Unable to fetch products.");
  }

  return response.json();
}

export default function ProductsScreen() {
  const {
    data,
    isPending,
    isError,
    error,
    refetch
  } = useQuery({
    queryKey: ["products"],
    queryFn: fetchProducts
  });

  if (isPending) {
    return ;
  }

  if (isError) {
    return (
      
        {error.message}
         refetch()}>Tap to retry
      
    );
  }

  return (
    
      {data.products.map((product) => (
        
          {product.title}
        
      ))}
    
  );
}

This example demonstrates basic query usage. For a production application, use a Pressable or Button for retry interactions and FlatList for long product lists. Query behavior and status names can vary by library version, so check the documentation for the installed version.

10.5 Understanding Query Keys

A query key uniquely identifies a query's cached data. Include relevant parameters when they change the result.

useQuery({
  queryKey: ["product", productId],
  queryFn: () => fetchProduct(productId)
});

Including productId in the key helps the library distinguish one product's cached result from another product's result.

10.6 Updating Data with Mutations

Use mutations for server operations such as creating, updating, or deleting records. After a successful mutation, invalidate or update the relevant cached queries so the UI reflects the latest server data.

const queryClient = useQueryClient();

const mutation = useMutation({
  mutationFn: updateProduct,
  onSuccess: () => {
    queryClient.invalidateQueries({
      queryKey: ["products"]
    });
  }
});

This is a pattern snippet. It assumes useQueryClient and useMutation are imported, updateProduct is implemented, and the component is inside QueryClientProvider.

11. Comparing Popular State Management Approaches

Approach Main Purpose Setup Best Fit Key Limitation
useState Local component state Minimal Simple UI and form values Not a shared global store
useReducer Structured local state transitions Minimal Complex component workflows Does not automatically share state globally
Context API Share values through a component tree Low Theme, locale, shared configuration No built-in server cache
Redux Toolkit Structured shared client state Moderate Complex applications and teams More concepts and setup
Zustand Lightweight shared state Low to moderate Shared app state with a small API Requires project conventions
TanStack Query Remote server-state management Moderate API data, caching, refetching Not intended to replace all client state
RTK Query API fetching and caching in Redux Toolkit Moderate Redux-based applications with substantial API usage Best fit when adopting its Redux-based model

12. State Management for Authentication

Authentication usually involves more than a boolean indicating whether a user is logged in. The application may need to represent initialization, an authenticated session, an unauthenticated session, and an authentication failure.

12.1 Recommended Authentication State

const initialAuthState = {
  status: "initializing",
  user: null
};

// Possible status values:
// "initializing"
// "authenticated"
// "unauthenticated"
// "error"

Modeling the status explicitly prevents the app from briefly displaying the login screen before it has checked whether a saved session is available.

12.2 Choosing an Approach

  • Context API may be sufficient for a small application with simple shared authentication state.
  • Redux Toolkit or Zustand may fit applications with more complex shared client state.
  • A server-state library can manage remote user-profile data, but it does not replace secure credential handling.
  • Use secure storage appropriate to the platform and sensitivity of tokens.
  • Enforce authorization on the server; hiding a screen in the client is not a security boundary.

12.3 Important Authentication Practices

  • Do not hardcode passwords or private API keys in the application.
  • Do not store sensitive tokens in unencrypted AsyncStorage.
  • Handle expired sessions and unauthorized responses consistently.
  • Clear relevant user-specific cached data when logging out.
  • Ensure navigation responds correctly to changes in authentication state.

13. State Management for Shopping Carts

A shopping cart often contains several related values: product IDs, quantities, item prices, discounts, and calculated totals. It is a good example of state that may need to be shared across multiple screens.

13.1 Choosing a Cart Approach

  • Small prototype: useReducer or Context API may be sufficient.
  • Lightweight app-wide store: Zustand may be a good fit.
  • Large application with complex shared state: Redux Toolkit may be appropriate.
  • Server-owned cart: Use API mutations and a server-state strategy to keep the displayed cart synchronized.

13.2 Store Derived Values Carefully

Totals can usually be calculated from cart items instead of being stored independently. This avoids inconsistencies when item quantities change.

const total = cartItems.reduce(
  (sum, item) => sum + item.price * item.quantity,
  0
);

For real purchases, the backend must validate prices, discounts, inventory, and the final payable amount. A client-calculated total is for display and is not a trusted payment value.

14. State Management for Forms

Forms may require values, validation errors, touched fields, submission status, and server validation messages. Small forms can use useState, while complex forms may benefit from a dedicated form library.

14.1 Simple Form State

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState("");

14.2 When to Use a Form Library

  • Many fields require validation.
  • Validation rules are complex or dependent on other fields.
  • Fields need touched, dirty, and error tracking.
  • The form includes dynamic arrays or nested objects.
  • Consistent submission and validation behavior is needed across screens.

Libraries such as React Hook Form can help organize form behavior, but their use should depend on the project's complexity and compatibility requirements.

15. State Persistence in React Native

Most React state exists in memory and is lost when the app process restarts. Persist only the information that needs to survive restarts, and decide how the application should restore it.

15.1 Common Persistence Options

Storage Type Suitable Data Important Consideration
AsyncStorage Non-sensitive preferences and simple cached values Unencrypted; not for secrets
Secure storage Sensitive credentials or tokens when appropriate Choose a maintained solution suitable for the target platforms
SQLite or another database Structured local records and larger datasets Requires schema and data lifecycle management
Server-side storage Account-owned information that must synchronize across devices Requires authentication, authorization, and connectivity handling

15.2 Persisting Non-Sensitive Preferences

import AsyncStorage from
  "@react-native-async-storage/async-storage";

async function saveTheme(theme) {
  await AsyncStorage.setItem("theme", theme);
}

async function loadTheme() {
  return AsyncStorage.getItem("theme");
}

Install and configure AsyncStorage using its official instructions. Handle storage failures and provide a sensible default if no saved preference exists.

15.3 Persistence Best Practices

  • Persist only necessary data.
  • Version or migrate stored data when the data structure changes.
  • Handle corrupt, missing, or outdated values safely.
  • Do not assume that cached server data is always current.
  • Clear user-specific persisted information on logout when required.
  • Use encrypted or platform-backed secure storage for sensitive credentials.

16. Performance and Re-Rendering

State changes can cause components to render again. The goal is not to prevent all re-renders but to keep them correct and efficient.

16.1 Keep State Close to Where It Is Used

Local state often reduces the number of components affected by a change. Avoid placing every temporary input value in a global store.

16.2 Split Contexts When Appropriate

When unrelated values change at different rates, separating them into different contexts can reduce unnecessary context-driven re-renders.

16.3 Use Selectors

Global stores often provide selectors that let a component subscribe to a specific part of the state. Select only the values the component needs.

16.4 Avoid Premature Optimization

  • Profile performance before making complex changes.
  • Use FlatList for large lists.
  • Keep state updates immutable and predictable.
  • Use memoization only when it provides a measurable benefit.
  • Choose appropriate cache policies for remote data.

17. Common State Management Mistakes

Mistake Why It Is a Problem Better Practice
Making every value global Creates unnecessary coupling. Keep local state local unless sharing is required.
Using Context for all API caching Requires custom cache and refetch logic. Consider a server-state library for remote data.
Mutating state directly Can lead to missed or incorrect updates. Create new objects and arrays or use supported immutable update tools.
Duplicating derived values Stored values can become inconsistent. Calculate values from their source state when practical.
Persisting sensitive data insecurely Exposes credentials to unnecessary risk. Use appropriate secure storage.
Storing all server responses forever Data may become stale or consume excess memory. Define cache freshness and invalidation rules.
Using multiple stores without a plan Creates unclear ownership of state. Document which tool owns each category of data.
Choosing a library only because it is popular Adds complexity without solving a real problem. Choose based on actual requirements.
Ignoring logout cleanup Another session may see stale user-specific data. Clear or invalidate the relevant state and cache.

18. Testing State Management

State management should be tested at the level where its behavior is defined. Reducers and pure state transformations can often be tested without rendering a screen. Components and API workflows need additional integration tests.

18.1 What to Test

  • Initial state is correct.
  • Actions produce the expected next state.
  • Invalid or unexpected actions are handled safely.
  • Loading, success, and error states render correctly.
  • API failures do not leave the interface stuck in loading.
  • Logout clears user-specific information as intended.
  • Persisted preferences restore correctly.
  • Concurrent requests do not display stale results.

18.2 Example: Testing a Reducer

const initialState = {
  value: 0
};

function counterReducer(state, action) {
  switch (action.type) {
    case "increment":
      return { ...state, value: state.value + 1 };
    case "reset":
      return initialState;
    default:
      return state;
  }
}

const nextState = counterReducer(initialState, {
  type: "increment"
});

console.log(nextState.value); // 1

This small example illustrates testing a pure state transition. In an actual test suite, use your project's testing framework and assert expected results rather than relying only on console output.

19. Example Application Architecture

A maintainable application assigns each type of state a clear owner instead of putting every value into one tool.

19.1 Suggested Folder Structure

MyMobileApp/
├── App.js
├── components/
│   ├── Button.js
│   └── LoadingView.js
├── screens/
│   ├── HomeScreen.js
│   ├── LoginScreen.js
│   └── ProductsScreen.js
├── features/
│   ├── auth/
│   │   ├── authSlice.js
│   │   └── authService.js
│   └── cart/
│       └── cartStore.js
├── services/
│   └── apiClient.js
├── hooks/
│   └── useProducts.js
├── storage/
│   └── preferences.js
└── navigation/
    └── AppNavigator.js

This is an illustrative structure. A small app may not need every folder, and the exact structure depends on the state management libraries and architecture chosen.

19.2 Example Ownership Plan

Data Possible Owner Reason
Open modal Component useState Only the current component needs it.
Theme Context or a small global store Many screens may need the same value.
Cart contents Reducer, Zustand, or Redux Toolkit Multiple screens may update shared cart state.
Product catalogue TanStack Query or RTK Query Remote data benefits from caching and refetching.
Current navigation route Navigation library The router owns navigation transitions.
Theme preference across restarts Theme state plus persistent storage In-memory state alone does not survive restarts.

20. Practical Mini Project: State Management for a Shopping App

20.1 Project Objective

Build a small React Native shopping application that displays products, manages a shared cart, remembers a theme preference, and loads product data from an API.

20.2 Functional Requirements

  1. Create a product listing screen.
  2. Retrieve products from a remote API.
  3. Display loading, success, error, and empty states.
  4. Allow users to add and remove products from a cart.
  5. Display the cart count in a header or tab.
  6. Calculate the subtotal from cart items.
  7. Share theme information across relevant screens.
  8. Persist the theme preference between app launches.
  9. Handle API failures with a retry option.
  10. Test logout and cleanup behavior if authentication is implemented.

20.3 Suggested State Choices

Feature Suggested Approach Reason
Search field useState Local interaction state.
Product data TanStack Query or RTK Query Remote data and caching.
Cart useReducer, Zustand, or Redux Toolkit Shared state and related updates.
Theme Context API or global store Shared application preference.
Saved theme AsyncStorage or another appropriate preference store Persistence across restarts.
Navigation Navigation library Owns route transitions.

20.4 Project Evaluation Checklist

  • State ownership is clear and documented.
  • Local UI state is not unnecessarily global.
  • Remote API data is not duplicated across several stores without a reason.
  • Updates are predictable and do not mutate state directly.
  • Loading and error handling are consistent.
  • Persistent data is appropriate and stored securely where required.
  • Components render correctly when state changes.
  • Tests cover normal behavior and failure cases.

21. Practice Exercises

  1. Create a counter using useState and explain why a global store is unnecessary.
  2. Build a multi-step form using useReducer.
  3. Create a ThemeContext that shares a theme across three screens.
  4. Build a shopping cart using useReducer or Zustand.
  5. Implement the same small counter using Redux Toolkit and compare the setup.
  6. Fetch a product list with TanStack Query and implement retry behavior.
  7. Persist a non-sensitive theme preference between application launches.
  8. Write reducer tests for add, remove, and reset actions.
  9. Implement logout behavior that clears user-specific state and cached information.
  10. Draw a state ownership diagram for a social media application.
  11. Explain which information belongs in navigation state, local state, and server state.
  12. Measure whether a state management change actually improves rendering performance.

22. Interview Questions and Answers

Q1. What is state management?

State management is the process of storing, updating, sharing, and organizing application data that affects behavior and the UI.

Q2. What is the difference between state and props?

State is managed by a component or state system and can change over time. Props are values passed from a parent component to a child.

Q3. What is local state?

Local state is information needed by a single component or a small, closely related part of the component tree.

Q4. What is global state?

Global state is shared information that is needed across different components or screens.

Q5. When should useState be used?

It is appropriate for simple local state, such as input values, visibility flags, and small UI interactions.

Q6. When should useReducer be used?

It is useful when state updates involve related values or multiple defined transitions.

Q7. What is Context API?

Context allows values to be shared with descendant components without manually passing props through every intermediate component.

Q8. Is Context API a complete replacement for Redux?

Not necessarily. Context shares values through a component tree, while Redux Toolkit provides a structured store and related tools for shared client state and debugging.

Q9. What is Redux Toolkit?

Redux Toolkit is the recommended standard approach for writing Redux logic with utilities that simplify store configuration, reducers, actions, and asynchronous workflows.

Q10. What is a Redux slice?

A slice defines a section of Redux state along with the reducers and actions that update it.

Q11. What is Zustand?

Zustand is a lightweight state management library that exposes shared state through a store and hooks.

Q12. What is server state?

Server state is data owned by a remote service and may become stale or change independently of the mobile app.

Q13. What is TanStack Query used for?

It manages server-state fetching, caching, refetching, request status, and cache invalidation.

Q14. What is the difference between client state and server state?

Client state belongs to the application's local interactions, while server state originates from an external system and needs synchronization with that system.

Q15. Does useState automatically persist data?

No. useState manages in-memory state. Persistence requires a separate storage strategy.

Q16. What is immutable state updating?

It means producing new objects or arrays for changed state rather than modifying existing state directly.

Q17. What are selectors?

Selectors retrieve particular values from a state store so components can subscribe to the data they need.

Q18. Why should derived values often not be stored separately?

They can be calculated from source state, avoiding duplicated information that could become inconsistent.

Q19. How should authentication state be managed?

Use a suitable shared state approach for session status and user information, store credentials securely, and enforce access control on the server.

Q20. How should a shopping cart be managed?

Use a predictable reducer or shared store when several screens need it, and derive totals from cart items when practical.

Q21. What is state persistence?

Persistence saves selected information outside in-memory state so it can be restored later.

Q22. Why should sensitive tokens not be stored in AsyncStorage?

AsyncStorage is unencrypted and is not designed for secret credential storage.

Q23. How can unnecessary re-renders be reduced?

Keep state close to where it is used, select only required store values, split unrelated contexts when appropriate, and profile before optimizing.

Q24. Should navigation state be stored in Redux?

Usually, the navigation library should own navigation state. Duplicating it in Redux is unnecessary unless a specific integration requirement justifies it.

Q25. How do you choose the right state management approach?

Identify the state category, consumers, update complexity, server-data requirements, persistence needs, performance constraints, and debugging needs. Choose the simplest approach that satisfies those requirements.

23. Quick Revision Checklist

  • State is information that can change and affect application behavior.
  • Use useState for simple local state.
  • Use useReducer for related state transitions.
  • Use Context API to share values through a component tree.
  • Use Redux Toolkit for structured shared client state when justified.
  • Consider Zustand for a lightweight shared store.
  • Use TanStack Query or RTK Query for appropriate server-state requirements.
  • Keep navigation state with the navigation system in most cases.
  • Use persistent storage only for information that needs to survive restarts.
  • Never treat unencrypted storage as secure credential storage.
  • Keep state updates predictable and avoid direct mutation.
  • Test state transitions, API failures, persistence, and logout cleanup.
  • Choose tools based on requirements rather than popularity alone.

24. Official Documentation and Learning Resources

25. Conclusion

Choosing the right state management approach is an important part of building maintainable React Native applications. Start by identifying whether the information is local UI state, shared client state, remote server data, navigation state, or persistent data.

Use React's built-in hooks for simple and well-contained state. Consider Context API for values shared across a component tree, Redux Toolkit or Zustand for more complex shared client state, and TanStack Query or RTK Query for server-state requirements. Persistence should be designed separately, with secure handling for sensitive information.

The most effective architecture is not necessarily the one with the most libraries. It is the one that makes state ownership clear, updates predictable, data secure, and the application easy to maintain as its requirements grow.

whatsapp